/*
 * WTextBox.java
 *
 * Created on 4 November 2007, 13:56
 *
 */

package net.ubinow.mflex.widgets;


import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;

import net.ubinow.mflex.baseui.AbsWidget;
import net.ubinow.mflex.baseui.WFont;
import net.ubinow.mflex.data.SimpleDataItem;
import net.ubinow.mflex.events.Event;
import net.ubinow.mflex.events.EventFactory;
import net.ubinow.mflex.util.Constants;
import net.ubinow.mflex.util.KeyConstants;

/**
 *
 * @author Josh
 */
public class WTextBox extends AbsWidget {
    
    //<editor-fold defaultstate="collapsed" desc="static contants">
    
    /** Max. milliseconds between cycling keypresses.  Longer than this and
     * a new character is produced
     */
    public static final int REPEAT_DELTA                                = 600;
    
    /** Milliseconds between cursor blinks */
    public static final int BLINK_RATE                                  = 500;
    
    /** Milliseconds after which, if a user has been holding down a key,
     * character entered will be replaced with the number of the key pressed.
     */
    public static final int HOLD_FOR_NUMBER_DUR                         = 1000;
    
    /** Milliseconds after which the last character entered in password
     * display mode will be replaced with an asterisk.
     */
    
    public static final int MASK_PASSWORD_CHAR_DUR                      = 1500;
    
    /**
     * Alphanumeric entry mode.
     * These are the characters produced by cycling through presses
     * on various handset keys:
     * 	   <ul>
     *      <li><b>0</b> key:  space ' ', and 0</li>
     *      <li><b>1</b> key:  . , ? ! 1 @ ' - _ ( ) : / * % # + < > =</li>
     *      <li><b>2</b> key:  a b c 2</li>
     *      <li><b>3</b> key:  d e f 3</li>
     *      <li><b>4</b> key:  g h i 4</li>
     *      <li><b>5</b> key:  j k l 5</li>
     *      <li><b>6</b> key:  m n o 6</li>
     *      <li><b>7</b> key:  p q r s 7</li>
     *      <li><b>8</b> key:  t u v 8</li>
     *      <li><b>9</b> key:  w x y z 9</li>
     *     </ul>
     */
    public static final int ENTRY_ALPHANUMERIC                          = 0;
    
    /**
     * Numeric mode. Numbers only (keys 0-9 produce numbers 0-9)
     */
    public static final int ENTRY_NUMERIC                               = 1;
    
    /**
     * Email Address mode. Same as "Alphanumeric" mode,
     * with the following exceptions:
     * 	   <ul>
     *      <li><b>0</b> key:  only 0</li>
     *      <li><b>1</b> key:  . 1 @ - _ +</li>
     *     </ul>
     */
    public static final int ENTRY_EMAILADDR                             = 2;
    
    /**
     * Username mode.  Same as "Alphanumeric", with the
     * following exceptions:
     * 	   <ul>
     *      <li><b>0</b> key:  only 0</li>
     *      <li><b>1</b> key:  1 _</li>
     *     </ul>
     */
    public static final int ENTRY_USERNAME                              = 5;
    
    /**
     * Full ASCII (ASCII chars 32-126, inclusive).
     * Same as "Alphanumeric" mode, with the following exceptions:
     * 	   <ul>
     *      <li><b>1</b> key:  . , ? ! 1 @ ' - _ ( ) : / * % # + < > = " $ & ; \ [ ] ^ ` | { } ~</li>
     *     </ul>
     */
    public static final int ENTRY_ASCII                                 = 6;
    
    /** Displays TextField contents unhidden */
    public static final int DISP_PLAINTEXT                              = 0;
    
    /** Displays TextField contents masked by cursors */
    public static final int DISP_PASSWORD                               = 1;
    
    private static final int DISP_PASSWORD_ENTRY                        = 2;
    
    // Key cycle tables
    private static final String TABLE 				= " .adgjmptw";
    private static final String TABLE_NUMERIC                   = "0123456789";
    private static final String[] REPEAT_TABLE                  = {
        " 0",
        ".,?!1@'-_():/*%#+<>=",
        "abc2",
        "def3",
        "ghi4",
        "jkl5",
        "mno6",
        "pqrs7",
        "tuv8",
        "wxyz9",
    };
    
    private static final String REPEAT_1KEY_EMAILADDR 	= ".1@-_+";
    private static final String REPEAT_1KEY_USERNAME 	= "1_";
    private static final String REPEAT_1KEY_ASCII 		= ".,?!1@'-_():/*%#+<>=\"$&;\\[]^`|{}~";
    
    private static final int TEXT_PADDING_X = 2;
    private static final int TEXT_PADDING_Y = 2;
    
    //</editor-fold>
    
    protected WFont _font = null;
    protected Image _imgNormal = null;
    protected Image _imgSelected = null;
    
    ///
    // text positioning
    ///
    protected int _textAlignment = 0;
    protected int _textX = -1;
    protected int _textY = -1;
    
    protected SimpleDataItem _text = null;
    
    protected int _maxSize;
    protected char[] _charArray;
    protected int _cursor;
    protected int _nChars;
    protected int _cursorX;
    protected int _startChar;
    protected int _lastKey;
    protected boolean _keyHeld;
    protected boolean _showCursor;
    protected int _repeatCount;
    protected int _stringWidth;
    protected long _lastKeyPress;
    protected boolean _upperCase;
    protected boolean _qwerty;
    protected int _entryMode = ENTRY_ALPHANUMERIC;
    protected int _dispMode = DISP_PLAINTEXT;
    protected int _cursorColour = 0x000000;
    
    public WTextBox( String id, WFont customFont, Image normal, Image selected ) {
        _id = id;
        
        _font = customFont;
        _imgNormal = normal;
        _imgSelected = selected;
        
        _width = _imgNormal.getWidth();
        _height = _imgNormal.getHeight();
        
        init();
    }
    
    /** Creates a new instance of WTextBox */
    public WTextBox( WFont customFont, Image normal, Image selected ) {
        this( Constants.UNDEFINED, customFont, normal, selected );
    }
    
    private void init(){
        // default textbox size
        _maxSize = Constants.AD_MAX_CHARS;
        
        _qwerty = false;
        _charArray = new char[_maxSize+1];
        _nChars = 0;
        _cursor = 0;
        _stringWidth = 0;
        _startChar = 0;
        
        // default alignment
        _textAlignment = Constants.ALIGN_CENTRE_V | Constants.ALIGN_LEFT;
        
        // init data item
        _text = new SimpleDataItem( "" );
        addItem( _text );
        _selectedIndex = 0;
        
        
    }
    
    //<editor-fold defaultstate="collpased" desc="public textbox methods">
    
    public void setAlignment( int alignment ){
        _textAlignment = alignment;
        
        // reset positioning
        _textX = _textY = -1;
    }
    
    /**
     * Gets the entry mode for this instance
     *
     * @return The entry mode for this instance
     */
    public int getEntryMode() {
        return _entryMode;
    }
    
    /**
     * Sets the entry mode for this instance
     *
     * @param mode The entry mode for this instance
     */
    public void setEntryMode( int mode) {
        this._entryMode = mode;
    }
    
    /**
     * Gets the display mode for this instance
     *
     * @return The display mode for this instance
     */
    public int getDispMode() {
        return _dispMode;
    }
    
    /**
     * Sets the display mode for this instance
     *
     * @param dispMode The display mode for this instance
     */
    public void setDispMode(int dispMode) {
        this._dispMode = dispMode;
    }
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="private methods">    
    
    private char toUpper(char c) {
        if (_upperCase && c >= 'a' && c <= 'z') c = (char)('A' + (c - 'a'));
        return c;
    }
    
    private void cycleInputHandling(){
        long time = System.currentTimeMillis();
            
            // Check for blink update
            if ((int)(time-_lastBlink) > BLINK_RATE) {
                _lastBlink = time;
                _showCursor = !_showCursor;
            }
            
            // Check for hold-for-number update (flip character to number
            // if key held down for long enough)
            if (_keyHeld && _lastKeyPress>0 && _cursor>0 &&
                    (int)(time-_lastKeyPress) > HOLD_FOR_NUMBER_DUR &&
                    _lastKey >= Canvas.KEY_NUM0 && _lastKey <= Canvas.KEY_NUM9
                    ) {
                _charArray[_cursor-1] = TABLE_NUMERIC.charAt( _lastKey-Canvas.KEY_NUM0);
                _stringWidth = _font.charsWidth( _charArray, 0, _nChars);
                _cursorX = _font.charsWidth( _charArray, 0, _cursor);
                _keyHeld = false;
                _lastKeyPress = time;
            }
            
            // Check for masking unmasked (freshly entered) characters in PASSWORD mode
            if ((int)(time-_lastKeyPress) > MASK_PASSWORD_CHAR_DUR &&
                    _dispMode == DISP_PASSWORD_ENTRY && _cursor>0) {
                _dispMode = DISP_PASSWORD;
            }
    }
    
    /**
     * Handles entry of a character from the keyboard or keypad.
     * The character entered depends on the entry mode, and 
     * keypad cycling behavior (if the time interval since the
     * last keypress was recent enough, then "repeat" will be
     * true and the last entered character will cycle once. 
     * Otherwise, a new character will be inserted at the
     * cursor position.)
     * 
     * @param repeat <code>true</code> if last key entered should
     *  cycle
     * @param key code of key entered
     * @return <code>true</code> if keypressed handled
     */
    private boolean enterCharacter( boolean repeat, int keyCode )
    {
    	String tableEntry = "";
        char c = ' ';
        boolean handled = false;
    	
    	// REPEAT:
        // Use repeat tables for cycling through keys (all modes except NUMERIC)
        if (repeat && keyCode >= Canvas.KEY_NUM0 && keyCode <= Canvas.KEY_NUM9) {
        	
        	// Use different "1" key repeat tables, depending on entry mode
        	if (keyCode == Canvas.KEY_NUM1) {
            	switch ( _entryMode ) {
            	case ENTRY_ALPHANUMERIC:tableEntry = REPEAT_TABLE[1]; 		break;
            	case ENTRY_EMAILADDR:	tableEntry = REPEAT_1KEY_EMAILADDR; break;
            	case ENTRY_USERNAME: 	tableEntry = REPEAT_1KEY_USERNAME; 	break;
            	case ENTRY_ASCII:		tableEntry = REPEAT_1KEY_ASCII; 	break;
            	}
            // For ALPHANUMERIC, EMAILADDR, and DOMAIN modes, keys other than "1" key all
            // share the same repeat tables.
        	} else {
        		tableEntry = REPEAT_TABLE[ keyCode - Canvas.KEY_NUM0];
        	}
            c = tableEntry.charAt(++_repeatCount % tableEntry.length());
            setChar( c, _cursor-1, true );
            handled = true;

        // NEW keypress:
        // Pick first entry in current mode's repeat tables, for given key pressed
        } else {
            _repeatCount = 0;
            if (_cursor <= _maxSize && ( keyCode == KeyConstants.SPACE || (keyCode >= 'a' && keyCode <= 'z') || 
            	(keyCode >= Canvas.KEY_NUM0 && keyCode <= Canvas.KEY_NUM9))) 
            {
            	// For "1" key, use first char in repeat table for current entry mode
              	if (keyCode == Canvas.KEY_NUM1) {
	            	switch (_entryMode) {
	            	case ENTRY_ALPHANUMERIC:c = REPEAT_TABLE[1].charAt(0); 		break;
	            	case ENTRY_NUMERIC: 	c = TABLE_NUMERIC.charAt(1); 		break;
	            	case ENTRY_EMAILADDR:	c = REPEAT_1KEY_EMAILADDR.charAt(0);break;
	            	case ENTRY_USERNAME: 	c = REPEAT_1KEY_USERNAME.charAt(0); break;
	            	case ENTRY_ASCII:		c = REPEAT_1KEY_ASCII.charAt(0);	break;
	            	}
	            // For all other keys: pick chars either from numeric or alphanumeric 
	            // first-char tables
              	} else {
		           	// NUMERIC mode - keys display their digit.  
              		// USERNAME and EMAILADDR modes - zero key only displays '0', never 'space' characer.
                	if (_entryMode == ENTRY_NUMERIC || 
                	    (_entryMode == ENTRY_USERNAME && keyCode == Canvas.KEY_NUM0) ||
                		(_entryMode == ENTRY_EMAILADDR && keyCode == Canvas.KEY_NUM0)
                		)
                		c = _qwerty ? (char)keyCode : TABLE_NUMERIC.charAt(keyCode - Canvas.KEY_NUM0);
                	// For all other modes use ALPHANUMERIC's beginning character
                	else 
                		c = _qwerty ? (char)keyCode : TABLE.charAt(keyCode - Canvas.KEY_NUM0);
              	}
              	insertChar( c, _cursor);
                handled = true;
            }
        }        
        
        return handled;
    }
    
    /**
     * Inserts a character at a given location in the string
     * 
     * @param c The character to insert
     * @param loc The index where it should be inserted.
     */
    private void insertChar( char c, int loc)
    {
    	if (_nChars >= _maxSize) return;
    	if (loc<0 || loc>=_maxSize) return;                
        
      	for (int i=_nChars; i>loc; i--) _charArray[i] = _charArray[i-1];
      	_nChars++;
    	_cursor++;
    	setChar( c, loc, false );
    }
    
    /**
     * Sets a character at a given location in the string
     * 
     * @param c The character to set
     * @param loc The index of the character to be overwritten with 'c'
     */
    private void setChar( char c, int loc, boolean replace )
    {
    	if (loc<0 || loc>=_maxSize) return;
    	c = toUpper(c);
      	_charArray[ loc] = c;
        
        if( loc < _text.getRawDataItem().length() ){
            if( replace )
                _text.getRawDataItem().setCharAt( loc, c );
            else
                _text.insertText( loc, c );
        }
        else
            _text.appendText( c );
        
    	if (_dispMode==DISP_PASSWORD) _dispMode=DISP_PASSWORD_ENTRY;
    	_stringWidth = _font.charsWidth( _charArray, _startChar, _nChars-_startChar);
       	_cursorX = _font.charsWidth( _charArray, _startChar, _cursor-_startChar);  
       	while (_cursorX > getWidth()-TEXT_PADDING_X-2) {
       		_startChar++;
       		_cursorX = _font.charsWidth(_charArray,_startChar,_cursor-_startChar);
       	}                        
    }

    /**
     * Removes a character at a given location in the string.  The
     * string will be compacted to close the gap.
     * @param loc The index where it should be removed.
     */
    private void deleteChar( int loc)
    {
    	if (loc<0 || loc>=_maxSize) return;
        
        if( _text.getRawDataItem().length() > 0 )
            _text.getRawDataItem().deleteCharAt( _cursor-1 );
//            _text.getRawDataItem().deleteCharAt( _cursor );
        
        for (int i=loc; i<_nChars-1; i++) _charArray[i] = _charArray[i+1];
        _cursor--;
        _nChars--;
        if (_startChar>0) _startChar--;
    	_stringWidth = _font.charsWidth( _charArray, _startChar, _nChars-_startChar);
       	_cursorX = _font.charsWidth( _charArray, _startChar, _cursor-_startChar);  
    	if (_dispMode==DISP_PASSWORD_ENTRY) _dispMode=DISP_PASSWORD;          
    }
    
    protected void realignTextX(){        
        // work out text position x
        if( (_textAlignment & Constants.ALIGN_LEFT) == Constants.ALIGN_LEFT ){
            _textX = _x + TEXT_PADDING_X;
        } else if( (_textAlignment & Constants.ALIGN_RIGHT) == Constants.ALIGN_RIGHT ){
            _textX = _x-TEXT_PADDING_X+(_width-_font.stringWidth( _text.getText() ) );
        } else if( (_textAlignment & Constants.ALIGN_CENTRE_H) == Constants.ALIGN_CENTRE_H ){
            _textX = _x + (_width-_font.stringWidth( _text.getText())>>1);
        }
    }
    
    protected void realignTextY(){
        // work out text position x
        if( (_textAlignment & Constants.ALIGN_TOP) == Constants.ALIGN_TOP ){
            _textY = _y + TEXT_PADDING_Y;
        } else if( (_textAlignment & Constants.ALIGN_CENTRE_V) == Constants.ALIGN_CENTRE_V ){
            _textY = _y + ((_height-_font.getHeight())>>1);
        } else if( (_textAlignment & Constants.ALIGN_BOTTOM) == Constants.ALIGN_BOTTOM ){
            _textY = _y+_height-TEXT_PADDING_Y-_font.getHeight();
        }
    }
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="overrides">
    
    public void setX( int x ){
        _textX= -1;
        super.setX( x );
    }
    
    public void setFocus( boolean focus ,int direction ){
        _startTime = _lastBlink = System.currentTimeMillis();
        
        super.setFocus( focus ,-1 );
        
        if( _focus )
            _showCursor = true;
        else
            _showCursor = false;
    }
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="Implement for AbsWidget">
    
    public void setText( String text ){
        // TODO: _text.setText( text );
        
        _nChars = _cursor = Math.min(text.length(), _maxSize);
        
        for (int i=0; i<_cursor; i++) {
            _charArray[i] = text.charAt(i);
        }                
        
        _stringWidth = _font.charsWidth(_charArray, 0, _nChars);
        _cursorX 	= _font.charsWidth(_charArray, 0, _nChars);
        System.out.println("setText..." + text + "...cur x = " + _cursorX );
        
        if (_dispMode==DISP_PASSWORD_ENTRY) _dispMode = DISP_PASSWORD;
        
        _text.setText( text );
        
        _checkTextLength = true; // set to true so that we force the next paint method to re-check that the text fits into the widget otherwise truncate it 
    }
    
    public String getText(){
        return _text.getText();
    }
    
    /** set the type of widget to widget type button **/
    protected void setWidgetType() {
        _type = Constants.WIDGET_TYPE_TEXTBOX;
    }
    
    public void setBG(Image bg) {
    }
    
    public void setBG(int colour) {
    }
    
    public int getCusorColour(){
        return _cursorColour;
    }
    
    public void setCursorColour( int colour ){
        _cursorColour = colour;
    }
    
    private long _startTime = System.currentTimeMillis();
    private long _lastBlink = 0;
    
    public void cycle() {
        
        if( _focus ){
            cycleInputHandling();                        
        }        
    }
    
    private boolean _checkTextLength = true; // set to true when the text length needs to be checked with the viewable length ie when the user sets the text or inputs characters - this is to ensure that only the visable characters are displayed 
    private int _charsToRemove = -1; // helper variables to help with the above condition ie when the text is too long for the visable width 

    public void paint(Graphics g, int offsetX, int offsetY, int layer) {
        if( !_isVisible )
            return;
        
        if( _focus ){
            g.drawImage( _imgSelected, _x + offsetX, _y + offsetY, Constants.GFX_ALIGN_TOP_LEFT );
        } else{
            g.drawImage( _imgNormal, _x + offsetX, _y + offsetY, Constants.GFX_ALIGN_TOP_LEFT );
        }                
        
        // realign text if necessary 
        if( _textX == -1 ){
            realignTextX();
        }
        
        if( _textY == -1 ){
            realignTextY();
        }        
                
        prepareClip( g, _x + offsetX, _y + offsetY, _width, _height );
        
        // draw text        
        char[] chars = _charArray;
        
        // TODO: more work required here; maybe we need to take into account the actual size of the character
        if( _checkTextLength ){
            int diff = _font.charsWidth(chars, _startChar, _nChars-_startChar );            
            if( diff > _width ){
                _charsToRemove = (diff-_width)/_font.getWidth();
                _charsToRemove+=2;
            }
            else{
                _charsToRemove = -1;
            }
            _checkTextLength = false; 
        }
        
        if( _charsToRemove >= 1 ){                        
            _font.drawChars( g, chars, _startChar, _nChars-_startChar-_charsToRemove, _textX + offsetX, _textY + offsetY );
        }
        else{
            _font.drawChars( g, chars, _startChar, _nChars-_startChar, _textX + offsetX, _textY + offsetY );
        }

       	// Draw cursor if text field has focus, and cursor is in "blink on"
        if (_focus && _showCursor) {
            g.setColor( _cursorColour );
            g.drawLine(
            		_textX + offsetX + _cursorX, 
            		_y + offsetY + 14, 
            		_textX + offsetX + _cursorX, 
            		_y + offsetY +  _height - 14
            		);
//            g.drawLine(
//            		_textX + offsetX + _cursorX, 
//            		_y + offsetY + 10, 
//            		_textX + offsetX + _cursorX, 
//            		_y + offsetY +  _height - 10
//            		);
        }        
        
        releaseClip( g );        
    }        
    
    /**
     * Update the state of this instance in light of a key press action.
     *
     * @param action The type of action. The TextField class ignores
     *               the action parameter, but responds to a wide variety
     *               of key types corresponding to text entry events.
     * @param key The key released.
     * @return true if the instance handled the event, and
     *         false if the instance ignored the event.
     */
    public boolean handleKeyPressed(int action, int keyCode) {
        boolean handled = false;
        int delta = 0;
        
        // Check if we're using a QWERTY keyboard intead of number pad
        if (keyCode == KeyConstants.SPACE || keyCode == KeyConstants.BACKSPACE || (keyCode >= 'a' && keyCode <= 'z')) _qwerty = true;
        
        // Check for key-cycling, if last press happened recently enough, and
        // we're in an entry mode that has key-cycling.
        delta = (int)(System.currentTimeMillis() - _lastKeyPress);
        boolean repeat =
                _entryMode != ENTRY_NUMERIC && 	// Don't repeat in NUMERIC mode
                delta < REPEAT_DELTA &&			// Don't repeat if last press was too long ago
                keyCode == _lastKey	&&				// Don't repeat if key pressed is different than last one
                !_qwerty &&						// Don't repeat if using QWERTY keyboard
                !(keyCode == Canvas.KEY_NUM0 && 		// Don't repeat on 0 key in USERNAME or
                (_entryMode == ENTRY_USERNAME ||	//  EMAILADDR entry modes
                _entryMode == ENTRY_EMAILADDR));
        
        if (_dispMode==DISP_PASSWORD_ENTRY) _dispMode=DISP_PASSWORD;
        
        // Left / Right
        if (keyCode == Canvas.LEFT || keyCode == KeyConstants.LEFT || keyCode == Canvas.RIGHT || keyCode == KeyConstants.RIGHT) {
            if (keyCode == Canvas.LEFT || keyCode == KeyConstants.LEFT) {
                if (_cursor>0) _cursor--;
                if (_cursor<_startChar) _startChar = _cursor;
            } else if (keyCode == Canvas.RIGHT || keyCode == KeyConstants.RIGHT) {
                if (_cursor<_nChars) _cursor++;
                
            }
            // Update start char and cursor pos
            _stringWidth = _font.charsWidth( _charArray, _startChar, _nChars-_startChar);
            _cursorX = _font.charsWidth( _charArray, _startChar, _cursor-_startChar);
            while (_cursorX > getWidth()-TEXT_PADDING_X-2) {
                _startChar++;
                _cursorX = _font.charsWidth(_charArray,_startChar,_cursor-_startChar);                
            }
            handled = true;            
        }
        
        // Pound
        else if (keyCode == Canvas.KEY_POUND) {
            _upperCase = !_upperCase;
            handled = true;
        }
        
        // Delete / Backspace
        else if (keyCode == KeyConstants.DELETE || keyCode == KeyConstants.BACKSPACE) {
            if (_cursor > 0) deleteChar( _cursor-1);
            handled = true;
        }
        
        // Fire / Enter
        else if (keyCode == Canvas.FIRE || keyCode == KeyConstants.ENTER) {
            Event evt = EventFactory.getInstance().getEvent( Event.EVT_TEXT_CHANGED, this );
            notifySubscribers( evt );
            handled = true;
        }
        
        // All other keys - either cycle through current key's character options,
        // or place a new character in the text field.
        else {
            handled = enterCharacter( repeat, keyCode );
        }
        
        _lastKey = keyCode;
        _keyHeld = true;
        _lastKeyPress = System.currentTimeMillis();
        _checkTextLength = true; // set to true so that we force the next paint method to re-check that the text fits into the widget otherwise truncate it
        
        return handled;
    }    
    
    /**
     * Turns off "press-for-number" behavior if user was holding
     * down a key.
     *
     * @param action The Canvas action code of the key released
     * @param key The keycode of the key released
     */
    public boolean handleKeyReleased(int action, int keyCode) {
        _keyHeld = false;
        return true;
    }
    
//</editor-fold>
    
}
